#! /usr/bin/perl -w

# click-elem2man -- creates man pages from structured comments in element
# source code
# Eddie Kohler
# Robert Morris - original make-faction-html script
#
# Copyright (c) 1999-2001 Massachusetts Institute of Technology
# Copyright (c) 2001-2003 International Computer Science Institute
# Copyright (c) 2006 Regents of the University of California
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, subject to the conditions
# listed in the Click LICENSE file. These conditions include: you must
# preserve this copyright notice, and you cannot mention the copyright
# holders in advertising related to the Software without their permission.
# The Software is provided WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED. This
# notice is a summary of the Click LICENSE file; the license in that file is
# legally binding.

my($verbose) = 0;

# information stored about each Click element, indexed by C++ class name
my(%processing, %deprecated, %portcount, %docrequires, %requires, $PREFIX);

my(%section_break) =
    ( 'head1' => 1, 'c' => 1, 's' => 1,
      'io' => 1, 'processing' => 1, 'drivers' => 1,
      'd' => 1, 'n' => 1, 'e' => 1, 'h' => 1, 'a' => 1, 'title' => 1,
      'deprecated' => 1, 'package' => 1 );
my(%section_takes_args) =
    ( 'head1' => 1, 'c' => 0, 's' => 2,
      'io' => 2, 'processing' => 2, 'drivers' => 2,
      'd' => 0, 'n' => 0, 'e' => 0, 'h' => 1, 'a' => 2, 'title' => 1,
      'deprecated' => 1, 'package' => 2 );
my(%section_takes_text) =
    ( 'head1' => 2, 'c' => 1, 's' => 1,
      'io' => 2, 'processing' => 2, 'drivers' => 2,
      'd' => 1, 'n' => 1, 'e' => 1, 'h' => 1, 'a' => 2, 'title' => 0,
      'deprecated' => 0, 'package' => 1 );
my(%xsection_takes_args) =
    ( 'head1' => 1, 'head2' => 1, 'item' => 1, 'over' => 1, 'back' => 0,
      'for' => 1, 'begin' => 1, 'end' => 1, 'start!' => 1, 'name!' => 1,
      'end!' => 0, 'text' => 0 );
my(%infosection_name) =
    ( 'package' => 'Package', 'io' => 'Ports', 'processing' => 'Processing',
      'drivers' => 'Drivers' );

my $section = 'n';
my $package;
my $uninstall = 0;
my(@all_outnames, %all_outsections, %summaries_by_section, %all_roff_summaries);

my(%processing_constants) =
    ( 'AGNOSTIC' => 'a/a', 'PUSH' => 'h/h', 'PULL' => 'l/l',
      'PUSH_TO_PULL' => 'h/l', 'PULL_TO_PUSH' => 'l/h' );
my(%processing_text) =
    ( 'a/a' => 'agnostic', 'h/h' => 'push', 'l/l' => 'pull',
      'h/l' => 'push inputs, pull outputs',
      'l/h' => 'pull inputs, push outputs',
      'a/ah' => 'agnostic, but output 1 is push' );

my(@section_headers) =
    ( 'basicsources'		=> 'Basic Sources and Sinks',
      'classification'		=> 'Basic Classification and Selection',
      'basictransfer'		=> 'Basic Packet Transfer',
      'counters'		=> 'Counters',
      'timestamps'		=> 'Timestamps',
      'basicmod'		=> 'Basic Packet Modification',
      'storage'			=> 'Packet Storage',
      'aqm'			=> 'Active Queue Management',
      'scheduling'		=> 'Packet Scheduling',
      'shaping'			=> 'Traffic Shaping',
      'information'		=> 'Information Elements',
      'netdevices'		=> 'Network Devices',
      'comm'			=> 'Host and Socket Communication',
      'ethernet'		=> 'Ethernet',
      'arp'			=> 'ARP',
      'ip'			=> 'IPv4',
      'iproute'			=> 'IPv4 Routing',
      'icmp'			=> 'ICMP',
      'nat'			=> 'Network Address Translation',
      'tcp'			=> 'TCP',
      'udp'			=> 'UDP',
      'app'			=> 'Applications',
      'traces'			=> 'Trace Manipulation',
      'ipmeasure'		=> 'TCP/IP Measurement',
      'aggregates'		=> 'Aggregates',
      'ip6'			=> 'IPv6',
      'ipsec'			=> 'IPsec',
      'crc'			=> 'CRCs',
      'paint'			=> 'Paint Annotations',
      'annotations'		=> 'Annotations',
      'debugging'		=> 'Debugging',
      'control'			=> 'Control',
      'smpclick'		=> 'Multithreaded Click',
      'test'			=> 'Regression Tests'
      );
my(%section_headers);
for (my $i = 0; $i < @section_headers; $i += 2) {
    $section_headers{$section_headers[$i]} = $section_headers[$i+1];
}

my(%default_packages) =
    ( 'analysis'		=> 'analysis (core)',
      'app'			=> 'app (core)',
      'aqm'			=> 'aqm (core)',
      'bsdmodule'		=> 'bsdmodule (core)',
      'ethernet'		=> 'ethernet (core)',
      'etherswitch'		=> 'etherswitch (core)',
      'grid'			=> 'grid (core)',
      'icmp'			=> 'icmp (core)',
      'ip'			=> 'ip (core)',
      'ip6'			=> 'ip6 (core)',
      'ipsec'			=> 'ipsec (core)',
      'linuxmodule'		=> 'linuxmodule (core)',
      'local'			=> 'local (core)',
      'ns'			=> 'ns (core)',
      'radio'			=> 'radio (core)',
      'simple'			=> 'simple (core)',
      'standard'		=> 'standard (core)',
      'tcpudp'			=> 'tcpudp (core)',
      'test'			=> 'test (core)',
      'userlevel'		=> 'userlevel (core)',
      'wifi'			=> 'wifi (core)'
      );

# find date
my($today) = '';
if (localtime =~ /\w*\s+(\w*)\s+(\d*)\s+\S*\s+(\d*)/) {
  $today = "$2/$1/$3";
}

# Unrolling [^A-Z>]|[A-Z](?!<) gives:    // MRE pp 165.
my $nonest = '(?:[^A-Z>]*(?:[A-Z](?!<)[^A-Z>]*)*)';


# XXX one paragraph at a time

my($Filename, $PrimaryElement, @Related, %RelatedSource, @Over, $Begun);


############
## shared ##

sub textize_add_links ($$) {
    my($t, $selfref) = @_;
    my($pos);

    # embolden & manpageize
    foreach my $r (@Related) {
	my $l = length($r);
	my $result = ($r eq $PrimaryElement ? "$selfref<$r>" : "L<$r>");
	for ($pos = index($t, $r); $pos >= 0; $pos = index($t, $r, $pos + 1)) {
	    if (($pos == 0 || substr($t, $pos - 1, 1) =~ /^[^\w@\/<]$/s)
		&& (substr($t, $pos + $l, 1) =~ /^[^\w@\/]$/s)) {
		substr($t, $pos, $l) = $result;
		$pos += 2 + $l;
	    }
	}
    }
    $t;
}

sub quote_unquoted_gt ($) {
    my($t) = @_;
    my($tt, $nest, $pos) = ('', 0);
    for ($pos = index($t, ">"); $pos >= 0; $pos = index($t, ">")) {
	my($w) = substr($t, 0, $pos);
	$nest++ while $w =~ m|[A-Z]<|g;
	if ($nest) {
	    $tt .= $w . ">";
	    $nest--;
	} else {
	    $tt .= $w . "E<gt>";
	}
	$t = substr($t, $pos + 1);
    }
    $tt . $t;
}


###########
## nroff ##

my $nroff_prologue = <<'EOD;';
.de M
.IR "\\$1" "(\\$2)\\$3"
..
.de RM
.RI "\\$1" "\\$2" "(\\$3)\\$4"
..
EOD;
chomp $nroff_prologue;

my(%nroff_podentities) =
    ( 'lt' => '<', 'gt' => '>', 'amp' => '&', 'solid' => '/',
      'verbar' => '|', 'eq' => '=', 'star' => '*', 'lparen' => '(',
      'rparen' => ')', 'lbrack' => '[', 'rbrack' => ']', 'lbrace' => '{',
      'rbrace' => '}' );

sub nroff_quote ($) {
    my($x) = @_;
    $x =~ tr/\000-\177/\200-\377/;
    $x;
}

sub nroff_unentity ($) {
    my($x) = @_;
    $x =~ tr/\200-\377/\000-\177/;
    if ($x =~ /^\d+$/) {
	chr($x);
    } elsif ($nroff_podentities{$x}) {
	$nroff_podentities{$x};
    } else {
	print STDERR "click-elem2man: $Filename: unknown entity E<$x>\n";
	"";
    }
}

sub nroff_fixfP ($$) {
    my($x, $f) = @_;
    $x =~ s/\\fP/\\f$f/g;
    $x;
}

sub nroffize_text ($) {
    my($t) = @_;
    my($s);

    # embolden & manpageize
    $t = textize_add_links($t, 'L');
  
    $t =~ s/\\/\\\\/g;
    $t =~ s/^\./\\&./gm;
    $t =~ s/^'/\\&'/gm;
    $t =~ s/^\s*$/.PP\n/gm;
    $t =~ s/^\.PP\n(?:\.PP\n)+/.PP\n/gm;
    $t =~ s{\A\s*(?:\.PP\n)?}{}s;
    if (@Over > 0) {
	$t =~ s/^\.PP$/.IP \"\" $Over[-1]/gm;
    }

    # get rid of entities
    $t =~ s{[\200-\377]}{"E<" . ord($1) . ">"}ge;
    $t =~ s{(E<[^>]*>)}{nroff_quote($1)}ge;
  
    my $maxnest = 10;
    while ($maxnest-- && $t =~ /[A-Z]</) {
    
	# can't do C font here
	$t =~ s/([BI])<($nonest)>/"\\f$1" . nroff_fixfP($2, $1) . "\\fP"/eg;
	$t =~ s/[PU]<($nonest)>/$1/g;
    
	# files and filelike refs in italics
	$t =~ s/F<($nonest)>/I<$1>/g;
    
	# LREF: man page references
	$t =~ s{L<($nonest)\(([\dln])\)>(\S*)(\s*)}{\n.M $1 $2 $3\n}g;
    
	$t =~ s/V<($nonest)>//g;
    
	# LREF: a la HREF L<show this text|man/section>
	$t =~ s{L<($nonest)>(\S*)(\s*)}{
	    if (($s = $RelatedSource{$1})) {
		"\n.M $1 \"$s\" $2\n";
	    } else {
		my $i = index($1, "|");
		($i >= 0 ? substr($1, 0, $i) . $2 . $3 : "\\fB$1\\fP$2$3");
	    }
	}eg;
    
	$t =~ s/Z<>/\\&/g;
	$t =~ s/N<>(\n?)/\n.br\n/g;

	# comes last because not subject to reprocessing
	$t =~ s/C<($nonest)>/"\\f(CW" . nroff_fixfP($1, 'R') . "\\fP"/eg;
    }

    # replace entities
    $t =~ s/\305\274([^\276]*)\276/nroff_unentity($1)/ge;
  
    # fix fonts
    $t =~ s/\\fP/\\fR/g;

    # fix manual
    $t =~ s/\n\.M (\S+) \"(\S+)\" (\(\2\))/\n.M $1 \"$2\" /sg;
    
    $t =~ s/\n+/\n/sg;
    $t =~ s/^\n+//;
    $t =~ s/\n+$//;

    $t;
}

sub nroffize ($) {
    my($t) = @_;
    # $t cannot contain \t, as all \ts have been expanded to spaces below

    #$t =~ s{\n[ \r]+$}{\n}gm;

    if ($t =~ /^ /m) {
	# worry about verbatims
	$t =~ s/^( .*)\n(\n+)(?= )/$1 . "\n" . (" \n" x length($2))/mge;
	my(@x) = split(/(^ .*$)/m, $t);
	my($o, $i) = '';
	for ($i = 0; $i < @x; $i += 2) {
	    if ($x[$i]) {
		$o .= nroffize_text($x[$i]) . "\n";
	    }
	    if ($x[$i+1]) {
		$x[$i+1] =~ s/\\/\\e/g;
		$o .= ".nf\n\\&" . $x[$i+1] . "\n.fi\n.PP\n";
	    }
	}
	$o =~ s/\n\.fi\n\.PP\n\n\.nf\n/\n/g;
	if (@Over > 0) {
	    $o =~ s/^\.PP$/.IP \"\" $Over[-1]/gm;
	}
	$o;
    } else {
	nroffize_text($t);
    }
}

sub nroff_do_section ($$$) {
    my($name, $args, $text) = @_;
    if (!exists($xsection_takes_args{$name})) {
	print STDERR "click-elem2man: $Filename: unknown section '=$name' ignored\n";
	return;
    }
    print STDERR "click-elem2man: $Filename: section '=$name' requires arguments\n"
	if ($xsection_takes_args{$name} && !$args);

    # handle '=begin' .. '=end'
    if ($name eq 'end') {
	undef $Begun;
    } elsif ($Begun && ($Begun eq 'man' || $Begun eq 'roff')) {
	print OUT '=', $name, ($args ? ' ' . $args : ''), "\n", $text;
	return;
    } elsif ($Begun) {
	return;
    }
  
    if ($name eq 'head1') {
	print OUT ".SH \"", nroffize($args), "\"\n";
    } elsif ($name eq 'head2') {
	print OUT ".SS \"", nroffize($args), "\"\n";
    } elsif ($name eq 'over') {
	if ($args =~ /^\s*(\d+)\s*$/s) {
	    print OUT ".RS $Over[-1]\n" if @Over;
	    push @Over, $1;
	} else {
	    print STDERR "click-elem2man: $Filename: bad arguments to '=over'\n";
	}
    } elsif ($name eq 'item') {
	if (@Over == 0) {
	    print STDERR "click-elem2man: $Filename: '=item' outside any '=over' section\n";
	} else {
	    print OUT ".IP \"", nroffize($args), "\" $Over[-1]\n";
	}
    } elsif ($name eq 'back') {
	my($overarg);
	if ($args =~ /^\s*(\d+)\s*$/s) {
	    $overarg = $1;
	} elsif ($args !~ /^\s*$/s) {
	    print STDERR "click-elem2man: $Filename: bad arguments to '=back'\n";
	}
	if (@Over == 0) {
	    print STDERR "click-elem2man: $Filename: too many '=back's\n";
	} else {
	    my($over) = pop @Over;
	    print OUT ".RE\n" if @Over;
	    print OUT (@Over ? ".IP \"\" $Over[-1]\n" : ".PP\n");
	    print STDERR "click-elem2man: $Filename: '=back $overarg' paired with '=over $over'\n"
		if defined($overarg) && $over != $overarg;
	}
    } elsif ($name eq 'for') {
	my($fortext);
	if ($text =~ /^(.*)\n\s*\n(.*)$/s) {
	    ($fortext, $text) = ($1, $2);
	} else {
	    ($fortext, $text) = ($text, '');
	}
	if ($args =~ /^\s*(man|roff)\s*(.*)/) {
	    print OUT $2, $fortext;
	}
    } elsif ($name eq 'begin') {
	$Begun = $args;
	$Begun =~ s/^\s*(\S+).*/$1/;
	if ($Begun eq 'man' || $Begun eq 'roff') {
	    print OUT $text;
	}
	return;
    } elsif ($name eq 'start!') {
	print OUT <<"EOD;";
.\\" -*- mode: nroff -*-
.\\" Generated by 'click-elem2man' from '$Filename'
$nroff_prologue
.TH "\U$text\E" $args "$today" "Click"
EOD;
	return;
    } elsif ($name eq 'name!') {
	print OUT <<"EOD;";
.SH "NAME"
$args \\- $text
EOD;
	return;
    } elsif ($name eq 'end!') {
	return;
    }

    print OUT nroffize($text), "\n";
    print OUT "\n" if $name eq 'head1';
}


##############
## dokuwiki ##

my($dokuwiki_dl);

my(%dokuwiki_podentities) =
    ( 'lt' => '<', 'gt' => "\xFF\xFF", 'amp' => '&', 'solid' => '/',
      'verbar' => '|', 'eq' => '=', 'star' => '*', 'lparen' => '(',
      'rparen' => ')', 'lbrack' => '[', 'rbrack' => ']', 'lbrace' => '{',
      'rbrace' => '}' );
my($dokuwiki_sensitive_char) = "[!%'\\(\\)\\*\\+/?\\[\\\\\\]_\\{\\}]";

sub dokuwiki_unentity ($) {
    my($x) = @_;
    if ($x =~ /^\d+$/) {
	$x = pack 'U', $x;
	if ($x =~ /\A$dokuwiki_sensitive_char\Z/) {
	    "%%$x%%";
	} else {
	    $x;
	}
    } elsif ($dokuwiki_podentities{$x}) {
	$dokuwiki_podentities{$x};
    } else {
	print STDERR "click-elem2man: $Filename: unknown entity E<$x>\n";
	"";
    }
}

sub dokuwiki_quotedbl ($) {
    my($x) = @_;
    $x =~ s/^(.)/\%\%$1\%\%/;
    $x;
}

sub dokuwiki_surround ($$) {
    my($text, $chr) = @_;
    my($re) = "\\$chr\\$chr";
    $text =~ s{$re}{}g;
    "$chr$chr$text$chr$chr";
}

sub dokuwikiize_text ($) {
    my($t) = @_;

    # embolden & manpageize
    $t = textize_add_links($t, 'P');
    
    # get rid of entities
    $t =~ s{E<([^>]*)>}{dokuwiki_unentity($1)}ge;
    $t =~ s{($dokuwiki_sensitive_char)}{^$1^}g;

    my $maxnest = 10;
    while ($maxnest-- && $t =~ /[A-Z]</) {
    
	# can't do C font here
	$t =~ s/B<($nonest)>/dokuwiki_surround($1, "*")/eg;
	$t =~ s.I<($nonest)>.dokuwiki_surround($1, "/").eg;
	$t =~ s.U<($nonest)>.dokuwiki_surround($1, "_").eg;
	$t =~ s/P<($nonest)>/$1/g;
	$t =~ s/C<($nonest)>/dokuwiki_surround($1, "'")/eg;
    
	# files and filelike refs in italics
	$t =~ s/F<($nonest)>/I<$1>/g;
    
	# LREF: man page references
	$t =~ s{L<($nonest)\^\(\^[\dln]\^\)\^>}{[[$1]]}g;
	$t =~ s{L<($nonest)>\^\(\^[\dln]\^\)\^}{[[$1]]}g;
    
	# LREF: a la HREF L<show this text|man/section>
	$t =~ s{L<($nonest)\|($nonest)>}{[[$2|$1]]}g;
	$t =~ s{L<($nonest)>}{[[$1]]}g;

	$t =~ s/V<($nonest)>//g;
	$t =~ s/Z<>//g;
	$t =~ s/N<>(\n?)/\\\\ /g;
    }
    
    $t =~ s/\n+/\n/sg;
    $t =~ s/^\n+//;
    $t =~ s/\n+$//;
    $t =~ s/\xFF\xFF/>/g;

    $t =~ s{^(\s*)\^([?!])\^}{$1 . dokuwiki_quotedbl($2)}egm;
    $t =~ s{($dokuwiki_sensitive_char)\^\1\^}{$1 . dokuwiki_quotedbl($1)}eg;
    $t =~ s{\^($dokuwiki_sensitive_char)\^\1}{dokuwiki_quotedbl($1) . $1}eg;
    $t =~ s{\^($dokuwiki_sensitive_char)\^}{$1}g;

    # remove self references
    $t =~ s{\[\[$PrimaryElement\]\]}{$PrimaryElement}g;

    $t;
}

sub dokuwikiize ($) {
    my($t) = @_;
    # $t cannot contain \t, as all \ts have been expanded to spaces below

    if ($t =~ /^ /m) {
	# worry about verbatims
	$t =~ s/^( .*)\n(\n+)(?= )/$1 . "\n" . (" \n" x length($2))/mge;
	my(@x) = split(/(^ .*$)/m, $t);
	my($o, $i) = '';
	for ($i = 0; $i < @x; $i += 2) {
	    if ($x[$i]) {
		$o .= dokuwikiize_text($x[$i]);
		$o .= "\n" if $o !~ /\n\Z/;
	    }
	    if ($x[$i+1]) {
		$o .= "  " . $x[$i+1] . "\n";
	    }
	}
	$o;
    } else {
	dokuwikiize_text($t);
    }
}

sub dokuwiki_do_section ($$$) {
    my($name, $args, $text) = @_;
    if (!exists($xsection_takes_args{$name})) {
	print STDERR "click-elem2man: $Filename: unknown section '=$name' ignored\n";
	return;
    }
    print STDERR "click-elem2man: $Filename: section '=$name' requires arguments\n"
	if ($xsection_takes_args{$name} && !$args);

    # handle '=begin' .. '=end'
    if ($name eq 'end') {
	undef $Begun;
    } elsif ($Begun && $Begun eq 'dokuwiki') {
	print OUT '=', $name, ($args ? ' ' . $args : ''), "\n", $text;
	return;
    } elsif ($Begun) {
	return;
    }
    
    if ($name eq 'head1') {
	print OUT "\n===== ", dokuwikiize($args), " =====\n\n";
    } elsif ($name eq 'head2') {
	print OUT "\n==== ", dokuwikiize($args), " ====\n\n";
    } elsif ($name eq 'over') {
	if ($args =~ /^\s*(\d+)\s*$/s) {
	    push @Over, $1;
	} else {
	    print STDERR "click-elem2man: $Filename: bad arguments to '=over'\n";
	}
    } elsif ($name eq 'item') {
	if (@Over == 0) {
	    print STDERR "click-elem2man: $Filename: '=item' outside any '=over' section\n";
	} else {
	    print OUT "\n", ' ' x @Over, "? " if $dokuwiki_dl;
	    print OUT dokuwikiize("B<" . quote_unquoted_gt($args) . ">");
	    print OUT ($dokuwiki_dl ? "\n" : "\n\n");
	}
    } elsif ($name eq 'back') {
	my($overarg);
	if ($args =~ /^\s*(\d+)\s*$/s) {
	    $overarg = $1;
	} elsif ($args !~ /^\s*$/s) {
	    print STDERR "click-elem2man: $Filename: bad arguments to '=back'\n";
	}
	if (@Over == 0) {
	    print STDERR "click-elem2man: $Filename: too many '=back's\n";
	} else {
	    my($over) = pop @Over;
	    print STDERR "click-elem2man: $Filename: '=back $overarg' paired with '=over $over'\n"
		if defined($overarg) && $over != $overarg;
	}
    } elsif ($name eq 'for') {
	my($fortext);
	if ($text =~ /^(.*)\n\s*\n(.*)$/s) {
	    ($fortext, $text) = ($1, $2);
	} else {
	    ($fortext, $text) = ($text, '');
	}
	if ($args =~ /^\s*dokuwiki\s*(.*)/) {
	    print OUT $2, $fortext;
	}
    } elsif ($name eq 'begin') {
	$Begun = $args;
	$Begun =~ s/^\s*(\S+).*/$1/;
	if ($Begun eq 'dokuwiki') {
	    print OUT $text;
	}
	return;
    } elsif ($name eq 'start!') {
	return;
    } elsif ($name eq 'name!') {
	print OUT <<"EOD;";
====== $args Element Documentation ======

===== NAME =====

**$args** -- $text
EOD;
	return;
    } elsif ($name eq 'end!') {
	print OUT "\n\nGenerated by 'click-elem2man' from '$Filename' on $today.\n";
	return;
    }

    print OUT ' ' x @Over, "! " if $text =~ /\S/ && @Over && $dokuwiki_dl;
    print OUT dokuwikiize($text), "\n";
    print OUT "\n" if $name eq 'head1';
}


###########
## main  ##

my $do_section_func = \&nroff_do_section;
my $do_text_func = \&nroffize;
my $filename_func;

sub do_section ($$$) {
    my($name, $args, $text) = @_;
    my(@text) = split(/^(=\w.*)$/m, $text);
    push @text, '' if !@text;
    my($i);
    @Over = ();
    undef $Begun;
    for ($i = 0; $i < @text; ) {
	&$do_section_func($name, $args, $text[$i]);
	($name, $args) = ($text[$i+1] =~ /=(\w+)\s*(.*)/)
	    if ($i < @text - 1);
	$i += 2;
    }
    &$do_section_func('back', '', '') while @Over;
    print STDERR "click-elem2man: $Filename: '=begin' not closed by end of section\n"
	if $Begun;
}

sub process_processing ($) {
    my($t) = @_;
    return undef if !defined($t);
    if (exists($processing_constants{$t})) {
	$t = $processing_constants{$t};
    }
    $t =~ tr/\" \t//d;
    $t =~ s{\A([^/]*)\Z}{$1/$1};
    $processing_text{$t};
}

sub process_one_portcount ($$) {
    my($t, $type) = @_;
    if ($t eq '0') {
	"no ${type}s";
    } elsif ($t eq '-') {
	"any number of ${type}s";
    } elsif ($t eq '1') {
	"1 $type";
    } elsif ($t eq '=') {
	"the same number of ${type}s";
    } elsif ($t =~ /^=(\++)$/) {
	length($1) . " more $type";
    } elsif ($t =~ /^0?-1$/) {
	"at most 1 $type";
    } elsif ($t =~ /^0?-(.*)/) {
	"at most $1 ${type}s";
    } elsif ($t =~ /^(\d+)-$/) {
	"$1 or more ${type}s";
    } else {
	"$t ${type}s";
    }
}

sub process_portcount ($) {
    my($t) = @_;
    $t = "$t/$t" if $t !~ /\//;
    return 'none' if $t eq '0/0';
    my($i, $o) = split(/\//, $t);
    process_one_portcount($i, "input") . ", " . process_one_portcount($o, "output");
}

sub process_summary_section ($$) {
    my($summary, $file) = @_;
    my($i);
    foreach $i (split(/,/, $summary)) {
	$i =~ s/^\s*//;
	$i =~ s/\s*$//;
	$i = $section_headers{$i} if exists $section_headers{$i};
	next if !$i;
	push @{$summaries_by_section{$i}}, $file;
    }
}

sub process_drivers ($) {
    my($driv) = @_;
    my(@d, $d);
    foreach $d ('userlevel', 'linuxmodule', 'bsdmodule', 'ns') {
	push @d, $d if $driv =~ /\b$d\b/;
    }
    join(', ', @d);
}

sub insert_section (\@\@\@$$$$) {
    my($sn, $sa, $st, $i, $n, $a, $t) = @_;
    splice @$sn, $i, 0, $n;
    splice @$sa, $i, 0, $a;
    splice @$st, $i, 0, $t;
}

sub insert_section2 (\@\@\@$$$\%@) {
    my($sn, $sa, $st, $n, $a, $t, $fis, @x) = @_;
    my($pos);
    foreach $pos (@x) {
	if (exists $fis->{$pos}) {
	    insert_section(@$sn, @$sa, @$st, $fis->{$pos} + 1, $n, $a, $t);
	    $fis->{$n} = $fis->{$pos} + 1;
	    return;
	}
    }
}

sub process_comment ($$) {
    my($t, $filename) = @_;
    my($i);
    $Filename = $filename;

    # split document into sections
    my(@section_text, @section_args, @section_name, $bad_section, $ref);
    $ref = \$bad_section;
    while ($t =~ m{^=(\w+)( *)(.*)([\0-\377]*?)(?=^=\w|\Z)}mg) {
	if ($section_break{$1}) {
	    insert_section(@section_name, @section_args, @section_text,
			   @section_name, $1, $3, $4);
	    $ref = \$section_text[-1];
	} else {
	    $$ref .= '=' . $1 . $2 . $3 . $4;
	}
    }

    # check document for sectioning errors
    print STDERR "click-elem2man: $Filename: warning: comment does not start with section\n"
	if $bad_section;
    my(%num_sections, %first_in_section);
    foreach $i (0..$#section_name) {
	my($n) = $section_name[$i];
	print STDERR "click-elem2man: $Filename: warning: section '=$n' requires arguments\n"
	    if $section_takes_args{$n} == 1 && !$section_args[$i];
	print STDERR "click-elem2man: $Filename: warning: section '=$n' arguments ignored\n"
	    if $section_takes_args{$n} == 0 && $section_args[$i];
	print STDERR "click-elem2man: $Filename: warning: empty section '=$n'\n"
	    if $section_takes_text{$n} == 1 && !$section_text[$i];
	print STDERR "click-elem2man: $Filename: warning: section '=$n' text ignored\n"
	    if $section_takes_text{$n} == 0 && $section_text[$i] =~ /\S/;
	$num_sections{$n}++;
	$first_in_section{$n} = $i if $num_sections{$n} == 1;
    }
    foreach $i ('a', 'c', 'd', 'n', 'e', 'title', 'io', 'processing', 'drivers') {
	print STDERR "click-elem2man: $Filename: warning: multiple '=$i' sections; some may be ignored\n"
	    if $num_sections{$i} && $num_sections{$i} > 1;
    }

    # read class names from configuration arguments section
    $i = $first_in_section{'c'};
    if (!defined($i)) {
	print STDERR "click-elem2man: $Filename: section '=c' missing; cannot continue\n";
	return;
    }
    my(@classes, %classes);
    while ($section_text[$i] =~ /^\s*(\w+)\(/mg) {
	push @classes, $1 if !exists $classes{$1};
	$classes{$1} = 1;
    }
    if (!@classes && $section_text[$i] =~ /^\s*([\w@]+)\s*$/) {
	push @classes, $1;
	$classes{$1} = 1;
    }
    if (!@classes) {
	print STDERR "click-elem2man: $Filename: no class definitions\n    (did you forget '()' in the =c section?)\n";
	return;
    }
    my($Title) = $classes[0];

    # output filenames might be specified in 'title' section
    my(@outfiles, @outsections, $title);
    if (defined($first_in_section{'title'})) {
	$title = $section_args[ $first_in_section{'title'} ];
	if (!$title) {
	    print STDERR "click-elem2man: $Filename: '=title' section present, but empty\n";
	    return;
	}
	if ($title =~ /[^-.\w@+,]/) {
	    print STDERR "click-elem2man: $Filename: strange characters in '=title', aborting\n";
	    return;
	}
	foreach $i (split(/\s+/, $title)) {
	    if ($i =~ /^(.*)\((.*)\)$/) {
		push @outfiles, $1;
		push @outsections, $2;
		$Title = $1;
	    } else {
		push @outfiles, $i;
		push @outsections, $section;
		$Title = $i;
	    }
	}
    } else {
	$title = join(', ', @classes);
	@outfiles = @classes;
	@outsections = ($section) x @classes;
    }

    # open new output file if necessary
    my($main_outname);
    if ($filename_func) {
	$main_outname = &$filename_func($outfiles[0], $outsections[0]);
	if ($uninstall) {
	    print STDERR "Uninstalled $main_outname\n" if $verbose;
	    unlink($main_outname);
	    open(OUT, ">/dev/null");
	} elsif (open(OUT, ">$main_outname")) {
	    print STDERR "Writing to $main_outname\n" if $verbose;
	} else {
	    print STDERR "$main_outname: $!\n";
	    return;
	}
    }
    push @all_outfiles, $outfiles[0];
    $all_outsections{$outfiles[0]} = $outsections[0];
    $PrimaryElement = $outfiles[0];

    # prepare related
    %RelatedSource = ();
    $i = $first_in_section{'a'};
    if (defined($i)) {
	$section_text[$i] = $section_args[$i] . $section_text[$i]
	    if $section_args[$i];
	if ($section_text[$i] =~ /\A\s*(.*?)(\n\s*\n.*\Z|\Z)/s) {
	    my($bit, $last) = ($1, $2);
	    while ($bit =~ m{(\b[A-Z][-\w@.+=]+)([,\s]|\Z)}g) {
		$RelatedSource{$1} = 'n';
	    }
	    $bit =~ s{([-\w@.+=]+)([,\s]|\Z)}{$1(n)$2}g;
	    while ($bit =~ m{([-\w@.+=]+\(([0-9ln])\))}g) {
		$RelatedSource{$1} = $2;
	    }
	    $section_text[$i] = $bit . $last;
	}
    }
    map(delete $RelatedSource{$_}, @outfiles);
    @Related = sort { length($b) <=> length($a) } (keys %RelatedSource, @classes);

    # front matter
    my($oneliner) = (@classes == 1 ? "Click element" : "Click elements");
    $i = $first_in_section{'s'};
    my($summary_section) = '';
    if (defined($i)) {
	$summary_section = $section_args[$i];
	process_summary_section($summary_section, $outfiles[0]);
	$section_text[$i] =~ s/\n\s*\n/\n/g;
	my($t) = &$do_text_func($section_text[$i]);
	$oneliner .= ";\n" . $t;
	$oneliner =~ s/\n(^\.)/ /g;
	$all_roff_summaries{$outfiles[0]} = $t . "\n";
    } else {
	# avoid uninitialized value warns
	$all_roff_summaries{$outfiles[0]} = '';
    }

    # deprecation
    if (defined($first_in_section{'deprecated'})) {
	$deprecated{$outfiles[0]} = $section_text[$first_in_section{'deprecated'}];
    }

    # package
    if (!defined($first_in_section{'package'}) && defined($package)) { PACKAGE: {
	my($pkg) = $package;
	if ($pkg eq 'DEFAULT') {
	    $pkg = $1 if $Filename =~ m|\belements/(\w+)/|;
	    $pkg = 'standard' if $Filename =~ m|\binclude/click/standard/|;
	    last PACKAGE if $pkg eq 'DEFAULT';
	    $pkg = exists($default_packages{$pkg}) ? $default_packages{$pkg} : $pkg;
	}
	insert_section2(@section_name, @section_args, @section_text,
			'package', '', $pkg,
			%first_in_section, 'drivers', 'processing', 'io', 'c');
    }}

    # drivers
    my($drivers);
    if ($docrequires{$Title}
	&& ($drivers = process_drivers($docrequires{$Title}))
	&& !defined($first_in_section{'drivers'})) {
	insert_section2(@section_name, @section_args, @section_text,
			'drivers', '', $drivers,
			%first_in_section, 'processing', 'io', 'c');
    }
	
    # processing
    if (!defined($first_in_section{'processing'})) { PROCESSING: {
	# can we figure out the processing type?
	last PROCESSING
	    if (defined($first_in_section{'io'}) && $section_text[$first_in_section{'io'}] =~ /None/i) || (defined($portcount{$Title}) && ($portcount{$Title} eq '0' || $portcount{$Title} eq '0/0'));
	my($ptype) = process_processing($processing{$Title});
	insert_section2(@section_name, @section_args, @section_text,
			'processing', '', $ptype,
			%first_in_section, 'io', 'c') if $ptype;
    }}

    # input/output
    if (!defined($first_in_section{'io'})) { PORTCOUNT: {
	last PORTCOUNT if !defined($portcount{$Title});
	insert_section2(@section_name, @section_args, @section_text,
			'io', '', process_portcount($portcount{$Title}),
			%first_in_section, 'c');
    }}

    # initial sections
    insert_section(@section_name, @section_args, @section_text,
		   0, 'start!', $outsections[0], $title);
    insert_section(@section_name, @section_args, @section_text,
		   1, 'name!', $title, $oneliner);
    insert_section(@section_name, @section_args, @section_text,
		   @section_name, 'end!', $outsections[0], $title);    

    # output
    my($special_section) = 0;
    for ($i = 0; $i < @section_text; $i++) {
	my($s) = $section_name[$i];
	my($x) = $section_text[$i];
	my($was_special_section) = $special_section;
	$special_section = 0;
	
	if ($s eq 'c') {
	    $x =~ s{(\S\s*)\n}{$1N<>\n}g;
	    $x =~ s{N<>\n*\z}{};
	    do_section('head1', 'SYNOPSIS', $x);
	} elsif ($s eq 'package' || $s eq 'io' || $s eq 'processing'
		 || $s eq 'drivers') {
	    $x =~ s/\A\s+//;
	    $x =~ s/\s+\Z//;
	    do_section('text', '', 'B<' . $infosection_name{$s} . '>: ' . $x . 'N<>');
	} elsif ($s eq 'io') {
	    do_section('head1', 'INPUTS AND OUTPUTS', $x);
	} elsif ($s eq 'processing') {
	    do_section('head1', 'PROCESSING TYPE', $x);
	} elsif ($s eq 'd') {
	    do_section('head1', 'DESCRIPTION', $x);
	} elsif ($s eq 'n') {
	    do_section('head1', 'NOTES', $x);
	} elsif ($s eq 'e') {
	    do_section('head1', 'EXAMPLES', $x);
	} elsif ($s eq 'h') {
	    my($t) = "=over 5\n";
	    while ($i < @section_text && $section_name[$i] eq 'h') {
		if ($section_args[$i] =~ /\A\s*(.*?)\s*"(.*?)"\s*\Z/
		    || $section_args[$i] =~ /\A\s*(.*?)\s*(\S+)\s*\Z/) {
		    $t .= "=item B<$1> ($2)\n";
		} else {
		    print STDERR "click-elem2man: $Filename: bad handler section arguments ('=h $section_args[$i]')\n";
		    $t .= "=item B<$section_args[$i]>\n";
		}
		$t .= $section_text[$i] . "\n";
		$i++;
	    }
	    $i--;
	    do_section('head1', 'ELEMENT HANDLERS', $t);
	} elsif ($s eq 'a') {
	    do_section('head1', 'SEE ALSO', $x);
	} elsif ($s eq 'title' || $s eq 's' || $s eq 'deprecated') {
	    # nada
	} else {
	    do_section($s, $section_args[$i], $x);
	}
    }

    # close output file & make links if appropriate
    if ($filename_func) {
	close OUT;
	for ($i = 1; $i < @outfiles; $i++) {
	    my($outname) = &$filename_func($outfiles[$i], $outsections[$i]);
	    print STDERR "Unlinking $outname\n" if $verbose;
	    unlink($outname);
	    if ($uninstall) {
		# do nothing
	    } elsif (link $main_outname, $outname) {
		print STDERR "Linked $outname\n" if $verbose;
		push @all_outfiles, $outfiles[$i];
		$all_outsections{$outfiles[$i]} = $outsections[$i];
		process_summary_section($summary_section, $outfiles[$i]);
		$all_roff_summaries{$outfiles[$i]} = $all_roff_summaries{$outfiles[0]};
	    } else {
		print STDERR "click-elem2man: $outname: $!\n";
	    }
	}
    }
}

sub process_file ($;$) {
    my($filename, $text) = @_;

    print STDERR "Attempting $filename...\n" if $verbose;
    $filename = "include/$1" if !-e $filename && $filename =~ /^<(.*)>$/s;
    $filename = "$PREFIX/$filename" if !-e $filename && defined($PREFIX);
    return if !-e $filename;
    print STDERR "Found $filename\n" if $verbose;
    if (!defined($text)) {
	if (!open(IN, $filename)) {
	    print STDERR "click-elem2man: $filename: $!\n";
	    return;
	}
	$text = <IN>;
	close IN;
    }

    print "$filename??\n" if !defined $text;
    foreach $_ (split(m{(/\*.*?\*/)}s, $text)) {
	if (/^\/\*/m && /^[\/*\s]+=/m) {
	    s/^(.*?)\t/$1 . (' ' x (8 - (length($1) % 8)))/egm while /\t/;
	    s/^\/\*\s*//g;
	    s/\s*\*\/$//g;
	    s/^ ?\* ?//gm;
	    process_comment($_, $filename);
	}
    }
}

sub xmlunquote ($) {
    my($x) = @_;
    if ($x =~ /&/) {
	$x =~ s/&lt;/</g;
	$x =~ s/&gt;/>/g;
	$x =~ s/&amp;/&/g;
	$x =~ s/&#([0-9]+);/chr($1)/eg;
	$x =~ s/&#x([0-9a-zA-Z]+);/chr(oct("0x$1"))/eg;
    }
    $x;
}

sub read_elementmap_text ($;$) {
    my($text, $headerhash) = @_;
    $headerhash = {} if !defined($headerhash);
    local($_);
    foreach $_ (split(/\n+/, $text)) {
	next if !/^<entry\b/;
	my($n) = (/\bname="(.*?)"/);
	my($dn) = (/\bdocname="(.*?)"/);
	my($p) = (/\bprocessing="(.*?)"/);
	my($hf) = (/\bheaderfile="(.*?)"/);
	my($pc) = (/\bportcount="(.*?)"/);
	my($req) = (/\brequires="(.*?)"/);
	if (defined($n) || defined($dn)) {
	    $dn = xmlunquote(defined($dn) ? $dn : $n);
	    $processing{$dn} = xmlunquote($p) if defined($p);
	    $portcount{$dn} = xmlunquote($pc) if defined($pc);
	    $req = defined($req) ? xmlunquote($req) : undef;
	    $docrequires{$dn} = $req if defined($req);

	    $n = $1 if defined($req) && !defined($n) && /\bprovides="(.*?)"/;
	    if (defined($n) && defined($req)) {
		$n = xmlunquote($n);
		$requires{$n} = [] if !defined $requires{$n};
		push @{$requires{$n}}, $req;
	    }
	    
	    $headerhash->{xmlunquote($hf)} = 1 if defined($hf);
	}
    }
}

sub read_elementmap ($;$) {
    my($fn, $headerhash) = @_;
    open(E, $fn) || die "$fn: $!\n";
    local($/) = undef;
    read_elementmap_text(<E>, $headerhash);
    close E;
}

sub process_file_set ($$) {
    my($fn, $fnhash) = @_;
    if (open(IN, ($fn eq '-' ? "<&STDIN" : $fn))) {
	my(@a, @b, $t, $x);
	$t = <IN>;
	close IN;
	
	# Parse file; click-buildtool gets special treatment
	if ($t =~ /\A#.*click-buildtool findelem/) {
	    $t =~ s/^#.*//mg;
	    foreach $_ (split(/\n+/, $t)) {
		if (/^\S+\s+<(\S+?)>/) {
		    $fnhash->{"include/$1"} = 1;
		} elsif (/^\S+\s+"(\S+?)"/) {
		    $fnhash->{$1} = 1;
		} else {
		    s/.cc$/.hh/;
		    $fnhash->{$_} = 1;
		}
	    }
	} elsif ($t =~ /\A<\?xml/) {
	    read_elementmap_text($t, $fnhash);
	} elsif ($t =~ /[\(\)]/) {
	    die if $fn eq '-';
	    $fnhash->{$fn} = 1;
	} else {
	    $t =~ s/^#.*//mg;
	    foreach $_ (split(/\s+/, $t)) {
		if ($t =~ /[*?\[]/) {
		    foreach my $x (glob($t)) {
			$fnhash->{$x} = 1;
		    }
		} else {
		    $fnhash->{$_} = 1;
		}
	    }
	}
    } else {
	print STDERR "click-elem2man: $fn: $!\n";
    }
}

sub uniq (@) {
    my(@a, $x);
    foreach $x (@_) {
	push @a, $x if !@a || $a[-1] ne $x;
    }
    @a;
}


sub driver_requirement ($) {
    my($req) = @_;
    my($d) = 0;
    $d |= 1 if $req =~ /\buserlevel\b/;
    $d |= 2 if $req =~ /\blinuxmodule\b/;
    $d |= 4 if $req =~ /\bbsdmodule\b/;
    $d |= 8 if $req =~ /\bns\b/;
    $d ? $d : 15;
}

my(%requires_fixed);
sub fix_one_requires ($) {
    my($req) = @_;
    my($driver) = driver_requirement($req);
    
    my($t, $i);
    my($changes) = '';
    foreach $t (split(/\s+/, $req)) {
	if (defined($requires{$t})) {
	    my($treq) = $requires{$t};
	    if (!$requires_fixed{$t}) {
		$requires_fixed{$t} = 1;
		for ($i = 0; $i < @$treq; $i++) {
		    $treq->[$i] = &fix_one_requires($treq->[$i]);
		}
	    }
	    for ($i = 0; $i < @$treq; $i++) {
		if ($driver & driver_requirement($treq->[$i])) {
		    $changes .= " " . $treq->[$i];
		}
	    }
	}
    }
    
    if ($changes) {
	join(' ', uniq(sort {$a cmp $b} split(/\s+/, $req . $changes)));
    } else {
	$req;
    }
}

sub fix_requires () {
    my($k);
    foreach $k (keys %docrequires) {
	$docrequires{$k} = fix_one_requires($docrequires{$k});
    }
}


# main program: parse options

sub usage () {
    print STDERR "Usage: click-elem2man [OPTIONS] FILES...
Try 'click-elem2man --help' for more information.\n";
    exit 1;
}

sub help () {
    print STDERR <<"EOD;";
'Click-elem2man' translates Click element documentation into manual pages.

Usage: click-elem2man [-l | -L] [-d DIRECTORY] FILE...

Each FILE is a Click header file, a list of Click header files, or the
output of click-mkelemmap.  '-' means standard input.

Options:
  -f, --files FILE        Read header filenames from FILE.
  -e, --elementmap EMAP   Read information about other elements from EMAP.
  -d, --directory DIR     Place generated manual pages in directory DIR.
  -P, --package PKG       Elements are in PKG package, or say 'DEFAULT'.
  -l, --list              Generate the elements(n) manual page as well.
  -L, --extend-list       Extend an existing elements(n) manual page.
  -p, --prefix PFX        Look for header files in PFX after looking in '.'.
      --dokuwiki          Generate dokuwiki source instead of manual pages.
      --dokuwiki-dl       Generate dokuwiki source using dl plugin.
  -u, --uninstall         Remove existing manual pages.
  -h, --help              Print this message and exit.

Report bugs to <click\@pdos.lcs.mit.edu>.
EOD;
    exit 0;
}

undef $/;
my(@files, $fn, $elementlist, $any_files, $directory, $output_type);
$output_type = 'nroff';

while (@ARGV) {
    $_ = shift @ARGV;
    if (/^-d$/ || /^--directory$/) {
	usage() if !@ARGV;
	$directory = shift @ARGV;
    } elsif (/^--directory=(.*)$/) {
	$directory = $1;
    } elsif (/^-P$/ || /^--package$/) {
	usage() if !@ARGV;
	$package = shift @ARGV;
    } elsif (/^-V$/ || /^--verbose$/) {
	$verbose = 1;
    } elsif (/^--package=(.*)$/) {
	$package = $1;
    } elsif (/^-f$/ || /^--files$/) {
	usage() if !@ARGV;
	push @files, shift @ARGV;
	$any_files = 1;
    } elsif (/^--files=(.*)$/) {
	push @files, $1;
	$any_files = 1;
    } elsif (/^-l$/ || /^--list$/) {
	$elementlist = 1;
    } elsif (/^-L$/ || /^--extend-list$/) {
	$elementlist = 2;
    } elsif (/^-e$/ || /^--elementmap$/) {
	usage() if !@ARGV;
	read_elementmap(shift @ARGV);
    } elsif (/^--elementmap=(.*)$/) {
	read_elementmap($1);
    } elsif (/^-p$/ || /^--prefix$/) {
	usage() if !@ARGV;
	$PREFIX = shift @ARGV;
    } elsif (/^--prefix=(.*)$/) {
	$PREFIX = $1;
    } elsif (/^-u$/ || /^--uninstall$/) {
	$uninstall = 1;
    } elsif (/^-h$/ || /^--help$/) {
	help();
    } elsif (/^--dokuwiki$/ || /^--doku$/) {
	$do_section_func = \&dokuwiki_do_section;
	$do_text_func = \&dokuwikiize;
	$output_type = 'dokuwiki';
    } elsif (/^--dokuwiki-dl$/) {
	$do_section_func = \&dokuwiki_do_section;
	$do_text_func = \&dokuwikiize;
	$output_type = 'dokuwiki';
	$dokuwiki_dl = 1;
    } elsif (/^-./) {
	usage();
    } elsif (/^-$/) {
	push @files, "-";
	$any_files = 1;
    } else {
	push @files, glob($_);
	$any_files = 1;
    }
}
push @files, "-" if !$any_files;

sub nroff_filename_func ($$) {
    my($name, $sec) = @_;
    "$directory/$name.$sec";
}

sub dokuwiki_filename_func ($$) {
    my($name, $sec) = @_;
    if ($sec eq 'n') {
	"$directory/" . lc($name) . ".txt";
    } else {
	"$directory/" . lc($name) . "-$sec.txt";
    }
}

if ($directory) {
    $filename_func = ($output_type eq 'dokuwiki' ? \&dokuwiki_filename_func : \&nroff_filename_func);
}

umask(022);
if ($uninstall) {
    open(OUT, ">/dev/null");
} elsif (!$directory) {
    open(OUT, ">&STDOUT");
}

# read file sets
my(%fnhash);
foreach $fn (@files) {
    process_file_set($fn, \%fnhash);
}
fix_requires();

# process files
foreach $fn (keys(%fnhash)) {
    process_file($fn);
}

if ($uninstall || !$directory) {
    close OUT;
}


# printing element list
#

sub read_elementlist ($) {
    my($fn) = @_;
  
    local($/) = "\n";
    if (!open(IN, $fn)) {
	print STDERR "click-elem2man: $fn: $!\n";
	return;
    }

    my($section, $active, $cur_name, $cur_section, %new_summary);
    $active = 0;
    while (<IN>) {
	if (/^\.SS \"(.*)\"/) {
	    $section = $1;
	} elsif (/^\.TP 20/) {
	    $active = 1;
	} elsif ($active == 1 && /^\.M (\S+) (\S+)/) {
	    if (!exists $all_outsections{$1}) {
		$active = 2;
		$cur_name = $1;
		$new_summary{$1} = 1;
		$all_outsections{$1} = $2;
		$all_roff_summaries{$1} = '';
		push @{$summaries_by_section{$section}}, $1;
		push @all_outfiles, $1;
	    } elsif ($new_summary{$1}) {
		push @{$summaries_by_section{$section}}, $1;
		$active = 3;
	    } else {
		$active = 3;
	    }
	} elsif (/^\.PD/) {
	    $active = 0;
	} elsif (/^\.SH \"ALPHABETICAL/) {
	    last;
	} elsif ($active == 2) {
	    $all_roff_summaries{$cur_name} .= $_;
	}
    }
}

my(%el_generated);
sub one_elementlist (@) {
    my($t);
    $t .= ".PP\n.PD 0\n";
    foreach $_ (sort { lc($a) cmp lc($b) } @_) {
	$t .= ".TP 20\n.M " . $_ . " " . $all_outsections{$_};
	$t .= " \" (deprecated)\"" if $deprecated{$_};
	$t .= "\n";
	$t .= $all_roff_summaries{$_} if $all_roff_summaries{$_};
	$el_generated{$_} = 1;
    }
    $t . ".PD\n";
}

my(@Links, $Text);

sub one_summary ($@) {
    my($name, @elts) = @_;
    print STDERR "click-elem2man: warning: category '", $name, "' begins with a lowercase letter (used in @elts)\n" if $name =~ /^[a-z]/;
    my($a) = $name;
    $a =~ s{([+\&\#\"\000-\037\177-\377])}{sprintf("%%%02X", $1)}eg;
    $a =~ tr/ /+/;
    my($x) = $name;
    $x =~ s{ }{&nbsp;}g;
    if (@elts) {
	push @Links, "<a href=\"#$a\">$x</a>";
	$Text .= ".SS \"$name\"\n";
	$Text .= one_elementlist(@elts);
    }
}

sub write_elementlist ($$) {
    my($enamebase, $l_uninstall) = @_;
    if ($filename_func) {
	my($fn) = &$filename_func($enamebase, $section);
	if ($l_uninstall) {
	    print STDERR "Removing $fn\n" if $verbose;
	    unlink($fn);
	    return;
	}
	if (!open(OUT, ">$fn")) {
	    print STDERR "click-elem2man: $fn: $!\n";
	    return;
	}
    } elsif ($uninstall) {
	return;
    }

    print OUT <<"EOD;";
.\\" -*- mode: nroff -*-
.\\" Generated by 'click-elem2man'
$nroff_prologue
.TH "\U$enamebase\E" $section "$today" "Click"
.SH "NAME"
$enamebase \- documented Click element classes
.SH "DESCRIPTION"
This page lists all Click element classes that have manual page documentation.
EOD;

    $Text = "";
    my(%did_section, $sec);
    # erase 'Miscellaneous' section, if any
    delete $summaries_by_section{'Miscellaneous'};
    my($i) = 0;
    foreach $sec ((map { $i++ % 2 ? ($_) : () } @section_headers),
		  (sort { lc($a) cmp lc($b) } keys(%summaries_by_section))) {
	next if $did_section{$sec};
	one_summary($sec, @{$summaries_by_section{$sec}});
	$did_section{$sec} = 1;
    }
    one_summary('Miscellaneous', grep { !$el_generated{$_} } @all_outfiles);

    my($links) = join("&nbsp;- ", @Links);
    print OUT <<"EOD;";
.\\"html <p><a href="#BY+FUNCTION"><b>By Function</b></a>:
.\\"html $links<br>
.\\"html <a href="#ALPHABETICAL+LIST"><b>Alphabetical List</b></a></p>
EOD;

    print OUT ".SH \"BY FUNCTION\"\n";
    print OUT $Text;
    print OUT ".SH \"ALPHABETICAL LIST\"\n";
    print OUT one_elementlist(@all_outfiles);

    close OUT if $filename_func;
}

sub read_main_elementlist () {
    if ($filename_func) {
	my($fn) = &$filename_func('elements', $section);
	$fn = &$filename_func('elements-click', $section) if !-r $fn;
	read_elementlist($fn) if -r $fn && $elementlist > 1;
    }
}

if ($elementlist && @all_outfiles) {
    # erase record of elements added on uninstall
    if ($elementlist > 1 && $uninstall) {
	%summaries_by_section = ();
	@all_outfiles = ();
    }

    # package-specific elementlist
    if (defined($package)) {
	my($fename) = ($package eq "DEFAULT" ? "elements-click" : "elements-$package");
	write_elementlist($fename, $uninstall);
    }
    read_main_elementlist();
    write_elementlist("elements", 0);
}
